﻿/* Copyright (C) 2008-2009 Jorson.WHY
 * All right reserved
 * ********** Project Info ***********
 * Project Name : MyLibrary.Text
 * NameSpace : MyLibrary.Text.WebPicker
 * Filename : WebPicker
 * Create Time : 4/14/2008 11:30:35 AM
 * ********** Environment Info *******
 * Machine Name : EA-01504-11220
 * CLR Version : 2.0.50727.1433
 */

namespace MyLibrary.Text.WebPicker
{
    #region System Using
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.IO;
    using System.Net;
    #endregion

    #region Summary
    /// <summary>
    /// 
    /// </summary>
    /// <remarks>
    /// 1、抓取远程网页源码，这里要实现自动判断网页编码，否则有可能抓到乱码。我是先看应答的http头的chareset，一般这个很准，但像csdn的新闻比较变态http应答的头里的chareset和网页的meta
    /// 里声明的chareset不一致，所以我手工加了一下判断，如果不一致再在内存流里用网页声明的编码读取一遍源码
    ///2、把网页分割成几大块。试用了一下tidy的.net包装及HtmlParse的.net版本，都不太好用。于是我自己写了个算法，可以把网页里的div块，td块等都提取出来，支持嵌套的情况。一般只提取div的
    ///文字块儿就行了。
    ///3、把汉字少于200的文本块去了，一般少于200字的文本块不会是正文，即便是正文，一般来说也不会有太多的价值，我直接去掉。
    ///4、因为div支持嵌套，所以剩下的文本块，有可能是重复的，一个是另一个的父节点，所以要把最里层的文本块找出来，最里层的文本块肯定是汉字最多的，而其它文本最少的，所以要计算出剩余文
    ///本中汉字占所有字符比例最高的文本块，基本上它就是正文的文本块了。当然有的网页正文里也可能还有div的文本块，这时候可能会判断错误，但只要正文嵌套的Div文本块的汉字少于200字，我的算
    ///法还是能准确提取正文文本块的。这一步我用写了一个自定义的方法传递给List的Sort方法。
    ///5、把<p><br>等标签替换成特殊占位符[p][br]等，因为最终的正文需要保留段落和回车换行等格式。这一步用正则实现。
    ///6、把最后剩下的文本块的html标签去掉，我用正则过滤的。
    ///7、吧[p]替换成回车换行加俩空格，吧[br]替换成回车换行，这步也用正则。到此，正文提取完毕
    /// </remarks>
    #endregion 
    public class HtmlContentPicker
    {
        private int CompareDinosByChineseLength(string x, string y)
        {
            if (String.IsNullOrEmpty(x))
            {
                if (String.IsNullOrEmpty(y))
                    return 0;
                else
                    return -1;
            }
            else
            {
                if (String.IsNullOrEmpty(y))
                    return 1;
                else
                {
                    Regex r = new Regex("[\u4e00-\u9fa5]");
                    float xCount = (float)(r.Matches(x).Count) / (float)x.Length;
                    float yCount = (float)(r.Matches(y).Count) / (float)y.Length;

                    int retval = xCount.CompareTo(yCount);
                    if (retval != 0)
                        return retval;
                    else
                        return x.CompareTo(y);
                }
            }
        }
        /// <summary>
        /// 在HTML流中查找指定的Tag内容
        /// </summary>
        /// <param name="input">HTML流</param>
        /// <param name="tag">需要查找的标签</param>
        /// <returns>指定标签的内容</returns>
        protected List<string> FindHtmlTags(string input, string tag)
        {
            StringReader reader = new StringReader(input);
            int characterInt = 0, tagCounter = 0, tagPosition = 0, counter = 0;
            char character = Char.MinValue;
            string tagString = String.Empty;

            Stack<int> tagPositionStack = new Stack<int>();
            List<string> tagList = new List<string>();

            while (true)
            {
                characterInt = reader.Read();
                if (characterInt == -1) break;

                character = Convert.ToChar(characterInt);
                //如果还存在有标签
                if (tagCounter > 0)
                {
                    //如果读取到的标签为'>'
                    if (character == '>')
                    {
                        //计算嵌套的标签数量
                        tagCounter--;
                        //读取标签
                        tagString = input.Substring(tagPosition, counter - tagPosition + 1);
                        //判断该标签是开始标签,还是结束标签
                        //开始标签 
                        if (tagString.ToLower().StartsWith(String.Format("<{0}", tag)))
                        {
                            //将符合比对标签的标签位置,压入标签栈
                            tagPositionStack.Push(tagPosition);
                        }
                        //结束标签
                        if (tagString.ToLower().StartsWith(String.Format("</{0}", tag)))
                        {
                            //如果标签栈中标签的数量为0,判断为错误内容
                            if (tagPositionStack.Count < 1) continue;
                            //弹出栈中找到的最后一个结束标签的位置
                            int tempTagPosition = tagPositionStack.Pop();
                            //将匹配到的标签中的内容加入到内容列表中
                            tagList.Add(input.Substring(tempTagPosition, counter - tempTagPosition + 1));
                        }
                    }
                }


                //如果读取到的字符为'<',表示标签的开始
                if (character == '<')
                {
                    tagCounter++;
                    tagPosition = counter;
                }

                counter++;
            }
            return tagList;
        }
        /// <summary>
        /// 从URL地址获取HTML数据
        /// </summary>
        /// <param name="url">URL地址</param>
        /// <returns>指定URL地址的HTML内容</returns>
        private string GetHtmlDataFromUrl(string url)
        {
            string html = String.Empty;
            HttpWebRequest request = null;
            HttpWebResponse response = null;

            try
            {
                request = (HttpWebRequest)HttpWebRequest.Create(url);

                //设置Http头
                request.AllowAutoRedirect = true;
                request.AllowWriteStreamBuffering = true;
                request.Referer = "";
                request.Timeout = 10 * 1000;
                request.UserAgent = "";
            }
            catch (UriFormatException uex)
            {
                throw new UriFormatException("指定的URL路径无效");
            }
            catch (Exception ex)
            {
                throw new ApplicationException("获取URL请求失败");
            }

            try
            {
                response = (HttpWebResponse)request.GetResponse();

                //如果得到了正确的回应
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    //判断编码
                    string charSet = response.CharacterSet;
                    if (String.IsNullOrEmpty(charSet))
                    {
                        if (charSet.ToLower().Equals("iso-8859-1")) 
                            charSet = "gb2312";
                    }


                    //保存回应的数据
                    Stream receiveStream = response.GetResponseStream();
                    MemoryStream mStream = new MemoryStream();

                    //将读取的数据流保存到一个内存流中
                    byte[] buffer = new byte[255];
                    int count = receiveStream.Read(buffer, 0, buffer.Length);
                    while (count > 0)
                    {
                        mStream.Write(buffer, 0, count);
                        count = receiveStream.Read(buffer, 0, buffer.Length);
                    }
                    receiveStream.Close();
                    //从内存流中读取HTML内容
                    html = ReadHtmlFromStream(mStream, charSet);
                    //从读取的HTTP字符串中判断CharSet,如果出现与头部不相同的情况,则重新读取
                    Regex regx = new Regex(@"<metal[\s\S]+?charset(.*)""[\s\S]+?>", RegexOptions.Multiline | RegexOptions.IgnoreCase);
                    MatchCollection mc = regx.Matches(html);

                    if (mc.Count > 0)
                    {
                        string otherCharSet = mc[0].Result("$1");
                        //重新读取
                        html = String.Empty;
                        html = ReadHtmlFromStream(mStream, otherCharSet);
                    }
                    mStream.Close();
                }
            }
            catch (Exception ex)
            {
                throw new ApplicationException("提取HTML数据时发生错误", ex);
            }
            finally
            {
                if (response != null)
                    response.Close();
            }
            return html;
        }

        public string GetMainContent(string url)
        {
            string reg1 = @"<(p|br)[^<]*>";
            string reg2 = @"(\[([^=]*)(=[^\]]*)?\][\s\S]*?\[/\1\])|(?<lj>(?=[^\u4E00-\u9FA5\uFE30-\uFFA0,."");])<a\s+[^>]*>[^<]{2,}</a>(?=[^\u4E00-\u9FA5\uFE30-\uFFA0,."");]))|(?<Style><style[\s\S]+?/style>)|(?<select><select[\s\S]+?/select>)|(?<Script><script[\s\S]*?/script>)|(?<Explein><\!\-\-[\s\S]*?\-\->)|(?<li><li(\s+[^>]+)?>[\s\S]*?/li>)|(?<Html></?\s*[^> ]+(\s*[^=>]+?=['""]?[^""']+?['""]?)*?[^\[<]*>)|(?<Other>&[a-zA-Z]+;)|(?<Other2>\#[a-z0-9]{6})|(?<Space>\s+)|(\&\#\d+\;)";

            //获取网页的内容
            string html = GetHtmlDataFromUrl(url);
            //获取网页中的Div标签
            List<string> tagContentList = FindHtmlTags(html,"div");
            //去除DIV中汉字少于300的DIV
            List<string> removeDiv = new List<string>();
            Regex chinese = new Regex("[\u4e00-\u9fa5]");
            foreach (string content in tagContentList)
            {
                if (content.Length < 300)
                    removeDiv.Add(content);
                else if (chinese.Matches(content).Count < 300)
                    removeDiv.Add(content);
            }
            foreach (string remove in removeDiv)
                tagContentList.Remove(remove);
            //?按照中文字符的多少进行排序
            tagContentList.Sort(CompareDinosByChineseLength);
            if (tagContentList.Count == 0)
                return String.Empty;
            html = tagContentList[tagContentList.Count - 1];
            //替换p和br
            html = new Regex(reg1, RegexOptions.Multiline | RegexOptions.IgnoreCase).Replace(html, "[$1]");
            //去掉HTML标签
            html = new Regex(reg2, RegexOptions.Multiline | RegexOptions.IgnoreCase).Replace(html, "[$1]");
            //将特殊占位符换成\r\n
            html = new Regex("\\[p]", RegexOptions.Multiline | RegexOptions.IgnoreCase).Replace(html, "\r\n");
            html = new Regex("\\[br]", RegexOptions.Multiline | RegexOptions.IgnoreCase).Replace(html, "\r\n");
            //去掉因为替换占位符造成的"[]"
            html = new Regex("\\[]", RegexOptions.Multiline | RegexOptions.IgnoreCase).Replace(html, "");
            //去掉文章头的回车换行
            html = html.TrimStart('\r', '\n');
            return html;
        }
        /// <summary>
        /// 以指定的字符集读取HTML内容
        /// </summary>
        /// <param name="stream">HTML的内存流</param>
        /// <param name="charSet">编码集</param>
        /// <returns>HTML内容</returns>
        protected string ReadHtmlFromStream(MemoryStream stream, string charSet)
        {
            if (stream == null) throw new ArgumentNullException("stream");
            if(String.IsNullOrEmpty(charSet)) throw new ArgumentNullException("charSet");

            int count = 0;
            string html = String.Empty;
            //确保从头读取内存流
            stream.Seek(0, SeekOrigin.Begin);

            StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(charSet));
            char[] buffer = new char[1024];
            count = reader.Read(buffer, 0, buffer.Length);
            while (count > 0)
            {
                html += new string(buffer, 0, count);
                count = reader.Read(buffer, 0, buffer.Length);
            }
            reader.Close();
            return html;
        }

    }
}

/*
 * 改进：
1、排序之前把超链接文字数量和汉字数量比例超过百分之50的div去掉，因为这些都是相关链接或者文字广告
2、把title分词（去除停止词，只保留名词和动词）后和剩下的几个div做比较，把内容里没有title分词的文字块去掉，因为正文内容一般都多多少少包含标题里的词。当然如果正文里用的都是比喻，拟人等手法，那也没办法了
3、代码强壮性增强，有些地方可能会抛异常。
4、好多论坛的地址都是一个Frame，所以获取源码的时候得不到目标页面的源码，这个其实可以加一个智能判断，580k就支持。
5、最后提取出的结果有时候不太赶紧，其实可以把最后剩下的文本块再做详细分析，去除掉无关文本。
6、目前只支持div布局的网站，还要加上支持表格布局的网站比如百度贴吧，达到自动检测。
 */
